/*
 * Copyright 2006 Matt Jensen
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package org.spectaql.internal.proxy.cglib;

import net.sf.cglib.proxy.*;
import org.spectaql.ProxyInstantiationStrategy;
import org.spectaql.metadata.EntityMetadata;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * <p>
 * <code>CglibProxyInstantiationStrategy</code> is an implementation of the {@link ProxyInstantiationStrategy}
 * interface which uses CGLib to generate dynamic subclass proxies for entity classes.
 * </p>
 */
public class CglibProxyInstantiationStrategy implements ProxyInstantiationStrategy
{
    /**
     * <p>
     * Map of {@link FactoryKey} instances describing entity class/handler interface pairs and their associated CGLib
     * {@link Factory} instances
     * </p>
     */
    private final ConcurrentMap<FactoryKey, Factory> m_proxyFactories = new ConcurrentHashMap<FactoryKey, Factory>();

    /**
     * <p>
     * Construct a {@link CglibProxyInstantiationStrategy} instance.
     * </p>
     */
    public CglibProxyInstantiationStrategy()
    {
        super();
    }

    /* inherit javadoc */
    @SuppressWarnings("unchecked")
    public <T> T createProxy(final EntityMetadata entityMetadata, final Object internalHandler)
    {
        assert null != entityMetadata : "The [entityMetadata] argument cannot be null.";
        assert null != internalHandler : "The [internalHandler] argument cannot be null.";

        // Get the internal interface class.
        final Class<?> internalInterface = getInternalInterface(internalHandler);

        // Get the factory for this proxy class and create a new instance.
        final Factory factory = getFactory(entityMetadata, internalInterface);

        // Create the instance callback array; it will be the same as the factory callback array except that the
        // placeholder callback will be replaced with a real callback which delegates to the supplied internal handler.
        final Callback[] factoryCallbacks = factory.getCallbacks();
        final Callback[] instanceCallbacks = new Callback[factoryCallbacks.length];
        System.arraycopy(factoryCallbacks, 0, instanceCallbacks, 0, factoryCallbacks.length);
        instanceCallbacks[ProxyCallbackFilter.CallbackType.INTERNAL.ordinal()] = new Dispatcher()
        {
            public Object loadObject() throws Exception
            {
                return internalHandler;
            }
        };

        // Create and return the proxy instance.
        return (T)factory.newInstance(instanceCallbacks);
    }

    /**
     * <p>
     * Get the {@link Factory} for use in creating proxies for a given entity class which implement a given handler
     * interface. The factory is created on first request for a entity class/handler interface pair, then cached and
     * reused for each subsequent request.
     * </p>
     *
     * @param entityMetadata   the entity metadata.
     * @param internalInterface the mixed-in internal interface.
     * @return {@link Factory} instance.
     */
    private Factory getFactory(final EntityMetadata entityMetadata, final Class<?> internalInterface)
    {
        // Check for a cached factory.
        final Class<?> entityClass = entityMetadata.getEntityClass();
        final FactoryKey key = new FactoryKey(entityClass, internalInterface);
        final Factory factory;
        if (m_proxyFactories.containsKey(key))
        {
            // Found a cached factory.
            factory = m_proxyFactories.get(key);
        }
        else
        {
            // Found no cached factory; create one now. First create a callback array which matches the indices expected
            // by ProxyCallbackFilter. This is somewhat verbose, but it minimizes the chances that the two classes will
            // go out of sync without triggering a compile-time error.
            final Callback[] callbacks = new Callback[ProxyCallbackFilter.CallbackType.values().length];
            callbacks[ProxyCallbackFilter.CallbackType.EQUALS.ordinal()] = EqualsMethodCallback.INSTANCE;
            callbacks[ProxyCallbackFilter.CallbackType.INTERNAL.ordinal()] = PLACEHOLDER_CALLBACK;
            callbacks[ProxyCallbackFilter.CallbackType.PASSTHROUGH.ordinal()] = NoOp.INSTANCE;
            callbacks[ProxyCallbackFilter.CallbackType.PROPERTY.ordinal()] =
                    new PropertyAccessorCallback(entityMetadata);
            callbacks[ProxyCallbackFilter.CallbackType.TOSTRING.ordinal()] = ToStringMethodCallback.INSTANCE;
            final CallbackFilter callbackFilter = new ProxyCallbackFilter(entityMetadata, internalInterface);

            // Create an enhancer and build the proxy class.
            final Enhancer enhancer = new Enhancer();
            enhancer.setCallbacks(callbacks);
            enhancer.setCallbackFilter(callbackFilter);
            enhancer.setInterfaces(new Class<?>[] { internalInterface });
            enhancer.setSuperclass(entityClass);

            // Create and attempt to cache the proxy class instance factory. If somebody else beats us to it, use their
            // factory instead.
            final Factory newFactory = (Factory)enhancer.create();
            final Factory existingFactory = m_proxyFactories.putIfAbsent(key, newFactory);
            if (null != existingFactory)
            {
                factory = existingFactory;
            }
            else
            {
                factory = newFactory;
            }
        }
        return factory;
    }

    /**
     * <p>
     * Get the internal interface implemented by a given handler. The handler must implement exactly one interface. Note
     * that the current implementation scans up the inheritance chain and stops at the first superclass which implements
     * at least one interface. This means that further superclasses could theoretically implement additional interfaces
     * which we will never see. There probably is no reason to go any further than this since we're dealing with
     * strictly internal classes.
     * </p>
     *
     * @param internalHandler the handler instance.
     * @return {@link Class} internal interface class.
     */
    private Class<?> getInternalInterface(final Object internalHandler)
    {
        // Scan up the inheritance chain until we find an interface.
        Class<?> result = null;
        for (Class<?> clazz = internalHandler.getClass(); null != clazz; clazz = clazz.getSuperclass())
        {
            final Class<?>[] interfaces = clazz.getInterfaces();
            if (interfaces.length > 1)
            {
                throw new IllegalArgumentException("The internal handler object must implement exactly one interface.");
            }
            else if (1 == interfaces.length)
            {
                result = interfaces[0];
                break;
            }
        }
        return result;
    }

    /**
     * <p>
     * Callback held in the delegate handler callback slot of the callback array for CGLib {@link Factory} instances.
     * For real proxy instances (created from the factory), this array slot is replaced by a real {@link Dispatcher}
     * which delegates to the handler object that was supplied to {@link #createProxy(EntityMetadata,Object)}.
     * </p>
     */
    private static final Dispatcher PLACEHOLDER_CALLBACK = new Dispatcher()
    {
        public Object loadObject() throws Exception
        {
            throw new IllegalStateException("Unexpected invocation of placeholder callback on factory instance.");
        }
    };

    /**
     * <p>
     * <code>FactoryKey</code> is used as a key in the proxy factory cache map. It bundles the proxied entity class with
     * the handler interface which is implemented by each proxy.
     * </p>
     */
    private static final class FactoryKey
    {
        /**
         * <p>
         * The entity class.
         * </p>
         */
        private final Class<?> m_entityClass;

        /**
         * <p>
         * The handler interface class.
         * </p>
         */
        private final Class<?> m_handlerInterface;

        /**
         * <p>
         * Construct a {@link FactoryKey} instance.
         * </p>
         *
         * @param entityClass      the entity class.
         * @param handlerInterface the handler interface class.
         */
        FactoryKey(final Class<?> entityClass, final Class<?> handlerInterface)
        {
            super();
            assert null != entityClass : "The [entityClass] argument cannot be null.";
            assert null != handlerInterface : "The [handlerInterface] argument cannot be null.";

            m_entityClass = entityClass;
            m_handlerInterface = handlerInterface;
        }

        /* inherit javadoc */
        public boolean equals(Object o)
        {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            FactoryKey that = (FactoryKey) o;

            if (!m_entityClass.equals(that.m_entityClass)) return false;
            if (!m_handlerInterface.equals(that.m_handlerInterface)) return false;

            return true;
        }

        /* inherit javadoc */
        public int hashCode()
        {
            int result;
            result = m_entityClass.hashCode();
            result = 31 * result + m_handlerInterface.hashCode();
            return result;
        }
    }
}
